/**
 *  BlueCove - Java library for Bluetooth
 *  Copyright (C) 2007-2009 Vlad Skarzhevskyy
 *
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 *
 *  @author vlads
 *  @version $Id: OBEXSessionBase.java 3010 2009-08-13 16:01:30Z skarzhevskyy $
 */
package com.intel.bluetooth.obex;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.Vector;

import javax.bluetooth.RemoteDevice;
import javax.bluetooth.ServiceRecord;
import javax.obex.Authenticator;
import javax.obex.HeaderSet;
import javax.obex.ServerRequestHandler;

import bt.javax.microedition.io.Connection;
import bt.javax.microedition.io.StreamConnection;

import com.intel.bluetooth.BluetoothConnectionAccess;
import com.intel.bluetooth.BluetoothStack;
import com.intel.bluetooth.DebugLog;
import com.intel.bluetooth.obex.OBEXAuthentication.Challenge;

/**
 * Base for Client and Server implementations. See <a
 * href="http://bluetooth.com/Bluetooth/Learn/Technology/Specifications/" >Bluetooth
 * Specification Documents</A> for details.
 * 
 */
abstract class OBEXSessionBase implements Connection, BluetoothConnectionAccess {

    protected boolean isConnected;

    private StreamConnection conn;

    private InputStream is;

    private OutputStream os;

    protected long connectionID;

    protected int mtu = OBEXOperationCodes.OBEX_DEFAULT_MTU;

    protected Authenticator authenticator;

    protected final OBEXConnectionParams obexConnectionParams;

    protected int packetsCountWrite;

    protected int packetsCountRead;

    private Vector authChallengesSent;

    /**
     * Each request packet flowed by response. This flag is from Client point of view
     */
    protected boolean requestSent;

    public OBEXSessionBase(StreamConnection conn, OBEXConnectionParams obexConnectionParams) throws IOException {
        if (obexConnectionParams == null) {
            throw new NullPointerException("obexConnectionParams is null");
        }
        this.isConnected = false;
        this.conn = conn;
        this.obexConnectionParams = obexConnectionParams;
        this.mtu = obexConnectionParams.mtu;
        this.connectionID = -1;
        this.packetsCountWrite = 0;
        this.packetsCountRead = 0;
        boolean initOK = false;
        try {
            this.os = conn.openOutputStream();
            this.is = conn.openInputStream();
            initOK = true;
        } finally {
            if (!initOK) {
                try {
                    this.close();
                } catch (IOException e) {
                    DebugLog.error("close error", e);
                }
            }
        }
    }

    public void close() throws IOException {
        StreamConnection c = this.conn;
        this.conn = null;
        try {
            try {
                if (this.is != null) {
                    this.is.close();
                    this.is = null;
                }
            } finally {
                // Close output even if input can't be closed.
                if (this.os != null) {
                    this.os.close();
                    this.os = null;
                }
            }
        } finally {
            // Close connection even if streams can't be closed.
            if (c != null) {
                c.close();
            }
        }

    }

    static OBEXHeaderSetImpl createOBEXHeaderSetImpl() {
        return new OBEXHeaderSetImpl();
    }

    public static HeaderSet createOBEXHeaderSet() {
        return createOBEXHeaderSetImpl();
    }

    static void validateCreatedHeaderSet(HeaderSet headers) {
        OBEXHeaderSetImpl.validateCreatedHeaderSet(headers);
    }

    protected void writePacket(int commId, OBEXHeaderSetImpl headers) throws IOException {
        writePacketWithFlags(commId, null, headers);
    }

    protected synchronized void writePacketWithFlags(int commId, byte[] headerFlagsData, OBEXHeaderSetImpl headers) throws IOException {
        if (this.requestSent) {
            throw new IOException("Write packet out of order");
        }
        this.requestSent = true;
        int len = 3;
        if (this.connectionID != -1) {
            len += 5;
        }
        if (headerFlagsData != null) {
            len += headerFlagsData.length;
        }
        byte[] data = null;
        if (headers != null) {
            data = OBEXHeaderSetImpl.toByteArray(headers);
            len += data.length;
        }
        if (len > mtu) {
            throw new IOException("Can't sent more data than in MTU, len=" + len + ", mtu=" + mtu);
        }
        this.packetsCountWrite++;
        ByteArrayOutputStream buf = new ByteArrayOutputStream();
        OBEXHeaderSetImpl.writeObexLen(buf, commId, len);
        if (headerFlagsData != null) {
            buf.write(headerFlagsData);
        }
        if (this.connectionID != -1) {
            OBEXHeaderSetImpl.writeObexInt(buf, OBEXHeaderSetImpl.OBEX_HDR_CONNECTION, this.connectionID);
        }
        if (data != null) {
            buf.write(data);
        }
        DebugLog.debug0x("obex send (" + this.packetsCountWrite + ")", OBEXUtils.toStringObexResponseCodes(commId), commId);
        os.write(buf.toByteArray());
        os.flush();
        DebugLog.debug("obex sent (" + this.packetsCountWrite + ") len", len);

        if ((headers != null) && (headers.hasAuthenticationChallenge())) {
            if (authChallengesSent == null) {
                authChallengesSent = new Vector();
            }
            for (Enumeration iter = headers.getAuthenticationChallenges(); iter.hasMoreElements();) {
                byte[] authChallenge = (byte[]) iter.nextElement();
                Challenge challenge = new Challenge(authChallenge);
                authChallengesSent.addElement(challenge);
            }
        }
    }

    protected synchronized byte[] readPacket() throws IOException {
        if (!this.requestSent) {
            throw new IOException("Read packet out of order");
        }
        this.requestSent = false;
        byte[] header = new byte[3];
        OBEXUtils.readFully(is, obexConnectionParams, header);
        this.packetsCountRead++;
        DebugLog.debug0x("obex received (" + this.packetsCountRead + ")", OBEXUtils.toStringObexResponseCodes(header[0]), header[0] & 0xFF);
        int lenght = OBEXUtils.bytesToShort(header[1], header[2]);
        if (lenght == 3) {
            return header;
        }
        if ((lenght < 3) || (lenght > OBEXOperationCodes.OBEX_MAX_PACKET_LEN)) {
            throw new IOException("Invalid packet length " + lenght);
        }
        byte[] data = new byte[lenght];
        System.arraycopy(header, 0, data, 0, header.length);
        OBEXUtils.readFully(is, obexConnectionParams, data, header.length, lenght - header.length);
        if (is.available() > 0) {
            DebugLog.debug("has more data after read", is.available());
        }
        return data;
    }

    private void validateBluetoothConnection() {
        if ((conn != null) && !(conn instanceof BluetoothConnectionAccess)) {
            throw new IllegalArgumentException("Not a Bluetooth connection " + conn.getClass().getName());
        }
    }

    void validateAuthenticationResponse(OBEXHeaderSetImpl requestHeaders, OBEXHeaderSetImpl incomingHeaders) throws IOException {
        if ((requestHeaders != null) && requestHeaders.hasAuthenticationChallenge() && (!incomingHeaders.hasAuthenticationResponses())) {
            // TODO verify that this appropriate Exception
            throw new IOException("Authentication response is missing");
        }
        handleAuthenticationResponse(incomingHeaders, null);
    }

    boolean handleAuthenticationResponse(OBEXHeaderSetImpl incomingHeaders, ServerRequestHandler serverHandler) throws IOException {
        if (incomingHeaders.hasAuthenticationResponses()) {
            if (authenticator == null) {
                throw new IOException("Authenticator required for authentication");
            }
            if ((authChallengesSent == null) && (authChallengesSent.size() == 0)) {
                throw new IOException("Authentication challenges had not been sent");
            }
            boolean authenticated = false;
            try {
                authenticated = OBEXAuthentication.handleAuthenticationResponse(incomingHeaders, authenticator, serverHandler, authChallengesSent);
            } finally {
                if ((authenticated) && (authChallengesSent != null)) {
                    authChallengesSent.removeAllElements();
                }
            }
            return authenticated;
        } else {
            if ((authChallengesSent != null) && (authChallengesSent.size() > 0)) {
                throw new IOException("Authentication response is missing");
            }
            return true;
        }
    }

    void handleAuthenticationChallenge(OBEXHeaderSetImpl incomingHeaders, OBEXHeaderSetImpl replyHeaders) throws IOException {
        if (incomingHeaders.hasAuthenticationChallenge()) {
            if (authenticator == null) {
                throw new IOException("Authenticator required for authentication");
            }
            OBEXAuthentication.handleAuthenticationChallenge(incomingHeaders, replyHeaders, authenticator);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.intel.bluetooth.BluetoothConnectionAccess#getRemoteAddress()
     */
    public long getRemoteAddress() throws IOException {
        validateBluetoothConnection();
        if (conn == null) {
            throw new IOException("Connection closed");
        }
        return ((BluetoothConnectionAccess) conn).getRemoteAddress();
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.intel.bluetooth.BluetoothConnectionAccess#getRemoteDevice()
     */
    public RemoteDevice getRemoteDevice() {
        validateBluetoothConnection();
        if (conn == null) {
            return null;
        }
        return ((BluetoothConnectionAccess) conn).getRemoteDevice();
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.intel.bluetooth.BluetoothConnectionAccess#isClosed()
     */
    public boolean isClosed() {
        if (conn == null) {
            return true;
        }
        if (this.conn instanceof BluetoothConnectionAccess) {
            return ((BluetoothConnectionAccess) conn).isClosed();
        } else {
            return false;
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.intel.bluetooth.BluetoothConnectionAccess#shutdown()
     */
    public void shutdown() throws IOException {
        if (this.conn instanceof BluetoothConnectionAccess) {
            ((BluetoothConnectionAccess) conn).shutdown();
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.intel.bluetooth.BluetoothConnectionAccess#markAuthenticated()
     */
    public void markAuthenticated() {
        validateBluetoothConnection();
        if (conn != null) {
            ((BluetoothConnectionAccess) conn).markAuthenticated();
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.intel.bluetooth.BluetoothConnectionAccess#getSecurityOpt()
     */
    public int getSecurityOpt() {
        validateBluetoothConnection();
        if (conn == null) {
            return ServiceRecord.NOAUTHENTICATE_NOENCRYPT;
        } else {
            return ((BluetoothConnectionAccess) conn).getSecurityOpt();
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.intel.bluetooth.BluetoothConnectionAccess#encrypt(boolean)
     */
    public boolean encrypt(long address, boolean on) throws IOException {
        validateBluetoothConnection();
        if (conn == null) {
            throw new IOException("Connection closed");
        }
        return ((BluetoothConnectionAccess) conn).encrypt(address, on);
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.intel.bluetooth.BluetoothConnectionAccess#setRemoteDevice(javax.bluetooth
     * .RemoteDevice)
     */
    public void setRemoteDevice(RemoteDevice remoteDevice) {
        validateBluetoothConnection();
        if (conn != null) {
            ((BluetoothConnectionAccess) conn).setRemoteDevice(remoteDevice);
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.intel.bluetooth.BluetoothConnectionAccess#getBluetoothStack()
     */
    public BluetoothStack getBluetoothStack() {
        validateBluetoothConnection();
        if (conn == null) {
            return null;
        }
        return ((BluetoothConnectionAccess) conn).getBluetoothStack();
    }

    /**
     * Function used in unit tests.
     * 
     * @return the packetsCountWrite
     */
    int getPacketsCountWrite() {
        return this.packetsCountWrite;
    }

    /**
     * Function used in unit tests.
     * 
     * @return the packetsCountRead
     */
    int getPacketsCountRead() {
        return this.packetsCountRead;
    }

    /**
     * Function used in unit tests.
     * 
     * @return the mtu
     */
    int getPacketSize() {
        if (isConnected) {
            return this.mtu;
        } else {
            return obexConnectionParams.mtu;
        }
    }

    /**
     * Function used to change the connection mtu
     * 
     * @param mtu
     * @throws IOException
     */
    void setPacketSize(int mtu) throws IOException {
        if (isConnected) {
            throw new IOException("Session already connected");
        }
        obexConnectionParams.mtu = mtu;
    }
}
